﻿using Microsoft.JScript.Vsa;
using System.Reflection;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.JScript;
using System.CodeDom.Compiler;
using System.Text.RegularExpressions;

// Following pragma is to remove the warnings over the use of obsolete Microsoft.Vsa
#pragma warning disable 612
namespace Scaredfinger.JSShell
{
  /// <summary>
  /// Defines a base for shells
  /// </summary>
  public interface IShell : IDisposable
  {
    /// <summary>
    /// Gets whether the shell is running
    /// </summary>
    bool Running { get; }

    /// <summary>
    /// Adds a variable to the global scope
    /// </summary>
    /// <param name="name">Variable name</param>
    /// <param name="value">Variable's value</param>
    void AddGlobalObject(string name, object value);

    /// <summary>
    /// Evaluates a java script line and return the result
    /// </summary>
    /// <param name="textline">Javascript block to execute. block : js expression (; js expression)*</param>
    /// <returns>block output, if any</returns>
    object Eval(string textline);

    /// <summary>
    /// Creates an Event Handler from a specified function body. This handler would work as an instance method of the shell. This means
    /// withing its body, "this" would refer to the shell's scope. Sender and event args are available as "sender" and "e" respectively.
    /// </summary>
    /// <typeparam name="TEventArgs">Event args type, to create a strongly typed handler</typeparam>
    /// <param name="functionBody">Java script function body, just the body, no declaration nor curlies</param>
    /// <returns>Delegate instance</returns>
    EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
      where TEventArgs : EventArgs;

    /// <summary>
    /// Opens this shell. Starts running it, readies it for work
    /// </summary>
    void OpenShell();

    /// <summary>
    /// Closes this shell. No more operations are posible.
    /// </summary>
    void CloseShell();
  }

  /// <summary>
  /// Provides a basic implementation using existing VSA elements.
  /// </summary>
  /// <remarks>
  /// Many features of <see cref="Microsoft.JScript"/> relay on the use of a <see cref="Microsoft.Vsa.IVsaEngine"/>, which is
  /// obsolete, but not yet replaced; it is also obscure (very), poorly documented, dificult to use and the worst: inextensible, too many 
  /// internals, some of them abstract.
  /// 
  /// For all these reassons my shells are not suposed to be created directly but instead using a <see cref="IShellFactory{TShell}"/>.
  /// 
  /// To Create a new Shell:
  /// <list type="numbered">
  ///   <item>Create a class that extends shell</item>
  ///   <item>Mark the methods you'd like to make available from the shell with <see cref="ShellOperationAttribute"/></item>
  ///   <item>Create a <see cref="ShellFactory{TShell}"/></item>
  ///   <item>Create a shell instance, <see cref="ShellFactory{TShell}.CreateShell()"/></item>
  /// </list>
  /// 
  /// Javascript naming convention is the same as Java (Camelcase with lower start letter). I prefer Pascal (Upper Camelcase) case,
  /// so my examples would be Pascal case.
  /// 
  /// Sample implementation:
  /// <code>
  /// 
  ///namespace SampleShell
  ///{
  ///  public class SampleShell : Shell
  ///  {
  ///    public TextWriter Output
  ///    {
  ///      get;
  ///      set;
  ///    }
  ///    
  ///    public SampleShell()
  ///      : this(Console.Out)
  ///    {
  ///    }
  ///    
  ///    public SampleShell(TextWriter output)
  ///    {
  ///      Output = output;
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Shows this help screen")]
  ///    public void Help()
  ///    {
  ///      foreach (var method in GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
  ///      {
  ///        var attr = method.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
  ///        
  ///        var parameters = string.Join(",",
  ///          (
  ///            from p in method.GetParameters()
  ///            select string.Format("{0} {1}", p.ParameterType.Name, p.Name)
  ///          ).ToArray()
  ///        );
  ///        
  ///        var text = string.Format("\t{0} {1}({2}):\t{3}", method.ReturnType.Name, method.Name, parameters,
  ///          attr == null ? "" : attr.Description);
  ///          
  ///        Print(text);
  ///      }
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Prints a text")]
  ///    public void Print(string text)
  ///    {
  ///      Output.WriteLine(text);
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Closes this shell")]
  ///    public void Exit()
  ///    {
  ///      CloseShell();
  ///    }
  ///  }
  ///}
  /// </code>
  /// </remarks>
  public class Shell : IShell
  {
    #region Constants

    /// <summary>
    /// String pattern for handler functions
    /// </summary>
    const string StrEventHandlerBody = @"
  var __anonymous_Handler = function(sender, e)
  {{
    {0};
  }}
";

    #endregion

    #region Fields

    /// <summary>
    /// Scripting engine. Behind this reference is the power of the dark side...
    /// </summary>
    VsaEngine Engine
    {
      get;
      set;
    }

    /// <summary>
    /// Represents a private scope which known this instance
    /// </summary>
    private ShellScope Scope
    {
      get;
      set;
    }

    /// <summary>
    /// Gets or sets whether the shell is closed
    /// </summary>
    private bool Closed
    {
      get;
      set;
    }

    #endregion

    #region IShell Members

    /// <inheritdoc/>
    public bool Running
    {
      get;
      private set;
    }

    /// <inheritdoc/>
    public void AddGlobalObject(string name, object value)
    {
      if (value == null)
        throw new ArgumentNullException("value");

      if (string.IsNullOrEmpty(name))
        throw new ArgumentException("'name' cannot be null nor empty");

      if (Scope.HasField(name))
        throw new DuplicatedGlobalObjectException(name);

      // Adding global object to scope
      Scope.CreateField(name, value);
    }

    /// <inheritdoc/>
    public object Eval(string source)
    {
      if (!Running)
        throw new ShellNotRunningException();

      // Overriding default scope with a ShellScope
      // This ways shell functions and user defined global objects
      // are available from the shell
      Engine.PushScriptObject(Scope);
      try
      {
        // Executing the evaluate
        return Microsoft.JScript.Eval.JScriptEvaluate(source, Engine);
      }
      finally
      {
        // Restoring default scope
        Engine.PopScriptObject();
      }
    }

    /// <inheritdoc/>
    public virtual EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
      where TEventArgs : EventArgs
    {
      // Compile the function
      var function = Eval(string.Format(StrEventHandlerBody, functionBody)) as ScriptFunction;

      // Check if everything went ok
      if (function == null)
        throw new BadHandlerException();

      // Create the delegate
      return (sender, e) => function.Invoke(Scope, new[] {sender, e});
    }

    /// <inheritdoc/>
    public void OpenShell()
    {
      if (Running)
        throw new ShellAlreadyRunningException();

      // Gets the Vsa Engine. Remember, this is the power of the dark side....
      var getEngine = GetType().GetMethod("GetEngine", BindingFlags.NonPublic | BindingFlags.Instance);
      Engine = (VsaEngine)getEngine.Invoke(this, new object[0]);

      // Creates a scope to override the global scope
      Scope = new ShellScope(this, Engine.GetMainScope());

      Running = true;
    }

    /// <inheritdoc/>
    public void CloseShell()
    {
      Running = false;

      if (Closed)
        return;

      Closed = true;
    }

    #endregion

    #region IDisposable Members

    /// <inheritdoc/>
    public void Dispose()
    {
      CloseShell();

      // Closing Darth Engine
      Engine.Close();
    }

    #endregion
  }

  /// <summary>
  /// A Block scope with reference to the shell object.
  /// </summary>
  /// <remarks>
  /// This scope would override global scope and make available all shell functions and defined global objects. For each function in 
  /// <see cref="Target"/> shell, there will be a variable with the same name in the scope. Global variables would be instance or static 
  /// fields in the Scope object. <see cref="CreateField"/>
  /// 
  /// Let's see following example:
  /// 
  /// Shell definition:
  /// <code>
  /// class MyShell : Shell
  /// {
  ///   public void Foo() {...}
  ///   public int Goo(int x) {...}
  /// }
  /// </code>
  /// 
  /// From the shell it would be like if have done:
  /// <code>
  /// var __this = new MyShell();
  /// var Foo = __this.Foo ;
  /// var Goo = __this.Goo ;
  /// </code>
  /// 
  /// And using it would be completely transparent:
  /// <code>
  /// > Foo()
  /// // What ever MyShell::Foo() might output
  /// > Goo(1); Goo(2); // And so
  /// // What ever ...you know
  /// </code>
  /// </remarks>
  internal class ShellScope : BlockScope
  {
    #region Static Tools

    static readonly Regex RexGuidCleaner = new Regex("[{}-]", RegexOptions.Compiled);
    static readonly Random RndIdGenerator = new Random();

    static string CreateScopeName()
    {
      return RexGuidCleaner.Replace(Guid.NewGuid().ToString(), "_");
    }

    static int CreateScopeId()
    {
      return RndIdGenerator.Next(100, int.MaxValue);
    }

    const string StrFunctionCallFormat = "{0}.{1};";

    #endregion

    #region Fields

    /// <summary>
    /// Keeps a reference to the shell
    /// </summary>
    private IShell Target
    {
      get;
      set;
    }

    /// <summary>
    /// Holds the name of current scope
    /// </summary>
    private string ScopeName
    {
      get;
      set;
    }

    /// <summary>
    /// Numeric id for current scope
    /// </summary>
    public int ScopeId
    {
      get;
      set;
    }

    /// <summary>
    /// To avoid any posible thinkable name clash
    /// </summary>
    private string ShellReference
    {
      get { return "__this" + ScopeName; }
    }

    /// <summary>
    /// Keeps track of scope variables
    /// </summary>
    private readonly Dictionary<string, FieldInfo> _dicMembers = new Dictionary<string, FieldInfo>();

    #endregion

    #region Creation && Initialization

    /// <summary>
    /// A completely flexible constructor
    /// </summary>
    /// <param name="shell">The shell execuing code</param>
    /// <param name="parent">The parent Vsa scope, currently the global scope</param>
    /// <param name="scopeName">A name for he scope</param>
    /// <param name="scopeId">An int id for the scope</param>
    ShellScope(IShell shell, ScriptObject parent, string scopeName, int scopeId)
      : base(parent, scopeName, scopeId)
    {
      ScopeName = scopeName;
      ScopeId = scopeId;
      Target = shell;
      CreateFields();
    }

    /// <summary>
    /// Creates a new scope
    /// </summary>
    /// <param name="shell">The shell execuing code</param>
    /// <param name="parent">The parent Vsa scope, currently the global scope</param>
    public ShellScope(IShell shell, ScriptObject parent)
      : this(shell, parent, CreateScopeName(), CreateScopeId())
    {
    }

    /// <summary>
    /// Creates a field for each <see cref="ShellOperationAttribute">shell operation</see>.
    /// </summary>
    private void CreateFields()
    {
      // Creating shell reference var
      CreateField(ShellReference, FieldAttributes.Public, Target);

      // Creating a field for each function
      foreach (var member in Target.GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
        CreateField(member.Name, FieldAttributes.Public, CreateFunction(member));
    }

    /// <summary>
    /// Creates a scriptfunction reference to assign to variables named after functions
    /// </summary>
    /// <param name="member">Function</param>
    /// <returns>ScriptFunction</returns>
    private ScriptFunction CreateFunction(MethodInfo member)
    {
      try
      {
        // Pushing current scope in the top
        // So shell reference is available
        engine.PushScriptObject(this);

        // Creating the ScriptFunction Object 
        return (ScriptFunction)Eval.JScriptEvaluate(
          string.Format(StrFunctionCallFormat, ShellReference, member.Name), engine);
      }
      finally
      {
        // Restoring current scope
        engine.PopScriptObject();
      }
    }

    #endregion

    #region Public Members

    /// <summary>
    /// Queries the existance of specified field
    /// </summary>
    /// <param name="name">Field name</param>
    /// <returns>true if it does, false if it doesn't</returns>
    public bool HasField(string name)
    {
      return _dicMembers.ContainsKey(name);
    }

    /// <summary>
    /// Creates a new field, a global var for instance
    /// </summary>
    /// <param name="name">Field name</param>
    /// <param name="value">Field value</param>
    public void CreateField(string name, object value)
    {
      CreateField(name, FieldAttributes.Public, value);
    }

    #endregion

    #region Overrides

    /// <summary>
    /// Creates a new "global" variable
    /// </summary>
    /// <param name="name">Variable name</param>
    /// <param name="attributeFlags">Field attributes for variable creation</param>
    /// <param name="value">Initial value</param>
    /// <returns>The reflection object for this variable</returns>
    protected override JSVariableField CreateField(string name, FieldAttributes attributeFlags, object value)
    {
      if (_dicMembers.ContainsKey(name))
        throw new DuplicatedGlobalObjectException(name);

      var result = base.CreateField(name, attributeFlags, value);
      _dicMembers.Add(name, result);

      return result;
    }

    /// <summary>
    /// Finds matching scope member. Overrides default behaviour returning members from internal dicionary
    /// </summary>
    /// <param name="name">Member name</param>
    /// <param name="bindingAttr">Attributes to refine search</param>
    /// <returns>Found member.</returns>
    public override MemberInfo[] GetMember(string name, BindingFlags bindingAttr)
    {
      return _dicMembers.ContainsKey(name) ? new MemberInfo[] { _dicMembers[name] } : base.GetMember(name, bindingAttr);
    }

    #endregion
  }
  ///------------------------------------------------------------------------------
  ///------------------------------------------------------------------------------
  ///------------------------------------------------------------------------------
  #region bottom
  /// <summary>
  /// Base interface for generating Shells
  /// </summary>
  /// <typeparam name="TShell"></typeparam>
  public interface IShellFactory<TShell>
    where TShell : class, IShell, new()
  {
    void AddReference(Assembly assembly);
    void AddImport(string import);

    TShell CreateShell();
  }

  /// <summary>
  /// 
  /// </summary>
  /// <typeparam name="TShell"></typeparam>
  /// <remarks>
  /// This implementation would create a JScript.NET class derived from <see cref="TShell"/>, then it would compile it using
  /// <see cref="JScriptCodeProvider"/>. Resulting assembly would be able to many <see cref="Microsoft.JScript"/> features, including
  /// the obscure ones.
  /// <code>
  /// var factory = new ShellFactory&lt;MyShell&gt;() ;
  /// 
  /// factory.AddReference(system) ;            // Creates a reference to assembly System
  /// factory.AddImport("System");              // Allows using System types from the shell
  /// factory.AddImport("System.Reflection") ;  // Allows using System.Reflection types from the shell
  /// </code>
  /// </remarks>
  public class ShellFactory<TShell> : IShellFactory<TShell>
    where TShell : class, IShell, new()
  {
    #region Constants

    /// <summary>
    /// Javascript import format string
    /// </summary>
    const string StrImport = "import {0};";

    /// <summary>
    /// Shell class format string
    /// </summary>
    const string StrShell = @"
{2}
import {1};
import System;

class __ShellImpl extends {1}.{0}
{{

}}
";

    static readonly Regex RexImportValidator = new Regex("^[a-zA-Z_]\\w*(\\.[a-zA-Z_]\\w*)*$", RegexOptions.Compiled);

    /// <summary>
    /// Gets a compiler info to create default compiler parameters later
    /// </summary>
    /// <seealso cref="System.CodeDom"/>
    /// <seealso cref="CodeDomProvider"/>
    /// <seealso cref="CompilerParameters"/>
    static readonly CompilerInfo JsCompilerInfo = CodeDomProvider.GetCompilerInfo("JScript");

    /// <summary>
    /// Creates a JS
    /// </summary>
    static readonly JScriptCodeProvider JsCompiler = new JScriptCodeProvider();

    /// <summary>
    /// Holds a Type instance for shell type
    /// </summary>
    static readonly Type ShellType = typeof(TShell);

    /// <summary>
    /// Holds a default (shared) factory instance. Might be useull when creating Shell instances from diferent scopes, this way
    /// you don't need to "save" a shared instance on your own. But be aware, changes on a factory affect al Shells created with 
    /// it.
    /// </summary>
    public static ShellFactory<TShell> Default = new ShellFactory<TShell>();

    #endregion

    readonly List<Assembly> _lstReferences = new List<Assembly>();

    public void AddReference(Assembly reference)
    {
      if (reference == null)
        throw new ArgumentNullException("reference");

      _lstReferences.Add(reference);
    }

    readonly List<string> _lstImports = new List<string>();

    public void AddImport(string import)
    {
      if (! RexImportValidator.Match(import).Success)
        throw new ImportFormatException(import);

      _lstImports.Add(import);
    }

    public TShell CreateShell()
    {
      // Creating default parameters for the JS crompiler
      var options = JsCompilerInfo.CreateDefaultCompilerParameters();

      // Adding referenced assemblies to compiler parameters
      options.ReferencedAssemblies.AddRange(
          (from x in _lstReferences
           select x.Location).ToArray()
        );

      // These 2 will always be referenced
      options.ReferencedAssemblies.Add(ShellType.Assembly.Location);
      options.ReferencedAssemblies.Add("mscorlib");

      // Creating imports text
      // import System ;
      // import System.Reflection;
      // stuff like that
      var imports = string.Join("\r\n", (from import in _lstImports
                                         select string.Format(StrImport, import)).ToArray());

      // Formats the actual shell class
      var source = string.Format(StrShell, ShellType.Name, ShellType.Namespace, imports);

      // Compiles the shell class
      var result = JsCompiler.CompileAssemblyFromSource(options, source);

      // There should be no compilation errors
      if (result.Errors.Count > 0)
        throw new CriticalException("Something has gone real wrong!!");

      return (TShell)Activator.CreateInstance(result.CompiledAssembly.GetType("__ShellImpl"));
    }
  }

  /// <summary>
  /// To mark the methods available from the shell
  /// </summary>
  [AttributeUsage(AttributeTargets.Method)]
  public class ShellOperationAttribute : Attribute
  {

  }
  #endregion
}
#pragma warning restore 612
